/*
 * This file is part of Hopsworks
 * Copyright (C) 2023, Hopsworks AB. All rights reserved
 *
 * Hopsworks is free software: you can redistribute it and/or modify it under the terms of
 * the GNU Affero General Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 *
 * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see <https://www.gnu.org/licenses/>.
 */
package io.hops.hopsworks.persistence.entity.util;

import io.hops.hopsworks.persistence.InvalidQueryException;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public abstract class AbstractFacade<T> {

  public static Integer BATCH_SIZE = 100;

  protected final Class<T> entityClass;

  public AbstractFacade(Class<T> entityClass) {
    this.entityClass = entityClass;
  }

  protected abstract EntityManager getEntityManager();

  public void save(T entity) {
    getEntityManager().persist(entity);
  }

  public T update(T entity) {
    return getEntityManager().merge(entity);
  }

  public void detach(T entity) { getEntityManager().detach(entity); }

  public void remove(T entity) {
    if (entity == null) {
      return;
    }
    getEntityManager().remove(getEntityManager().merge(entity));
    getEntityManager().flush();
  }

  public void removeBatch(List<T> entities) {
    for (T item : entities) {
      getEntityManager().remove(getEntityManager().merge(item));
    }

    getEntityManager().flush();
  }

  public T find(Object id) {
    return getEntityManager().find(entityClass, id);
  }

  public List<T> findAll() {
    javax.persistence.criteria.CriteriaQuery cq = getEntityManager().
        getCriteriaBuilder().createQuery();
    cq.select(cq.from(entityClass));
    return getEntityManager().createQuery(cq).getResultList();
  }

  public List<T> findRange(int[] range) {
    javax.persistence.criteria.CriteriaQuery cq = getEntityManager().
        getCriteriaBuilder().createQuery();
    cq.select(cq.from(entityClass));
    javax.persistence.Query q = getEntityManager().createQuery(cq);
    q.setMaxResults(range[1] - range[0]);
    q.setFirstResult(range[0]);
    return q.getResultList();
  }

  public long count() {
    javax.persistence.criteria.CriteriaQuery cq = getEntityManager().
        getCriteriaBuilder().createQuery();
    javax.persistence.criteria.Root<T> rt = cq.from(entityClass);
    cq.select(getEntityManager().getCriteriaBuilder().count(rt)).where();
    javax.persistence.Query q = getEntityManager().createQuery(cq);
    return (Long) q.getSingleResult();
  }

  public void setOffsetAndLim(Integer offset, Integer limit, Query q) {
    if (offset != null && offset > 0) {
      q.setFirstResult(offset);
    }
    if (limit != null && limit > 0) {
      q.setMaxResults(limit);
    }
  }

  public String orderBy(SortBy sortBy) {
    return sortBy.getSql() + " " +  sortBy.getParam().getSql();
  }

  public String buildQuery(String query, Set<? extends FilterBy> filters,
      Set<? extends AbstractFacade.SortBy> sorts, String more) {
    return query + buildFilterString(filters, more) + buildSortString(sorts);
  }

  public String buildSortString(Set<? extends SortBy> sortBy) {
    if (sortBy == null || sortBy.isEmpty()) {
      return "";
    }
    sortBy.remove(null);
    Iterator<? extends SortBy> sort = sortBy.iterator();
    if (!sort.hasNext()) {
      return "";
    }
    StringBuilder c = new StringBuilder(" ORDER BY " + orderBy(sort.next()));
    for (;sort.hasNext();) {
      c.append(", ").append(orderBy(sort.next()));
    }
    return c.toString();
  }

  public String buildFilterString(Set<? extends FilterBy> filter, String more) {
    String s = more == null || more.isEmpty() ? "" : "WHERE " + more;
    if (filter == null || filter.isEmpty()) {
      return s;
    }
    filter.remove(null);
    Iterator<? extends FilterBy> filterBy = filter.iterator();
    if (!filterBy.hasNext()) {
      return s;
    }
    StringBuilder c = new StringBuilder(" WHERE " + filterBy.next().getSql());
    for (;filterBy.hasNext();) {
      c.append(" AND ").append(filterBy.next().getSql());
    }
    return c.append(more == null || more.isEmpty()? "": " AND " + more).toString();
  }

  public Date getDate(String field, String value) {
    String[] formats = {"yyyy-MM-dd'T'HH:mm:ss.SSSX", "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ssX",
        "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:sss", "yyyy-MM-dd"};
    Date date = null;
    for (int i = 0; i < formats.length && date == null; i++ ) {
      date = getDateByFormat(value, formats[i]);
    }
    if (date == null) {
      throw new InvalidQueryException(
          "Filter value for " + field + " needs to set valid format. Expected:yyyy-MM-dd hh:mm:ss.SSSX but found: " +
              value);
    }
    return date;
  }

  private Date getDateByFormat(String value, String format) {
    SimpleDateFormat sdf = new SimpleDateFormat(format);
    try {
      return sdf.parse(value);
    } catch (ParseException e) {
      return null;
    }
  }

  public Integer getIntValue(FilterBy filterBy) {
    return getIntValue(filterBy.getField(), filterBy.getParam());
  }

  public Integer getIntValue(String field, String value) {
    Integer val;
    try {
      val = Integer.parseInt(value);
    } catch (NumberFormatException e) {
      throw new InvalidQueryException("Filter value for " + field + " needs to set an Integer, but found: " + value);
    }
    return val;
  }

  public Long getLongValue(String field, String value) {
    try {
      return Long.parseLong(value);
    } catch (NumberFormatException e) {
      throw new InvalidQueryException("Filter value for " + field + " needs to set a Long, but found: " + value);
    }
  }

  public List<Integer> getIntValues(FilterBy filterBy) {
    String[] filterStrs = splitFilterParams(filterBy);
    List<Integer> values = new ArrayList<>();
    String field = filterBy.getField();
    Integer val;
    for (String filterStr : filterStrs) {
      val = getIntValue(field, filterStr);
      values.add(val);
    }
    return values;
  }

  public boolean getBooleanValue(String value) {
    return "1".equals(value) || "true".equalsIgnoreCase(value);
  }

  public <E extends Enum<E>> List<E> getEnumValues(FilterBy filterBy, final Class<E> enumType) {
    String[] filterStrs = splitFilterParams(filterBy);
    List<E> enumObjects = new ArrayList<>();
    String field = filterBy.getField();
    E enumObject;
    for (String filterStr : filterStrs) {
      enumObject = getEnumValue(field, filterStr, enumType);
      enumObjects.add(enumObject);
    }
    return enumObjects;
  }

  public <E extends Enum<E>> E getEnumValue(String field, String filterStr, final Class<E> enumType) {
    E enumObject;
    try {
      enumObject = E.valueOf(enumType, filterStr);
    } catch (IllegalArgumentException iae) {
      throw new InvalidQueryException("Filter value for " + field + " needs to set valid " + field + ", but found: "
          + filterStr, iae);
    }
    return enumObject;
  }

  public String[] splitFilterParams(FilterBy filterBy) {
    return filterBy.getParam().split(",");
  }

  public interface SortBy {
    String getValue();
    OrderBy getParam();
    String getSql();
  }

  public interface FilterBy {
    String getValue();
    String getParam();
    String getSql();
    String getField();
  }

  public enum OrderBy {
    ASC ("ASC", "ASC"),
    DESC ("DESC", "DESC");

    private final String value;
    private final String sql;

    private OrderBy(String value, String sql) {
      this.value = value;
      this.sql = sql;
    }

    public String getValue() {
      return value;
    }

    public String getSql() {
      return sql;
    }

    @Override
    public String toString() {
      return value;
    }

  }

  public static class CollectionInfo<A> {
    private Long count;
    private List<A> items;

    public CollectionInfo(Long count, List<A> items) {
      this.count = count;
      this.items = items;
    }

    public Long getCount() {
      return count;
    }

    public List<A> getItems() {
      return items;
    }

    public void setItems(List<A> items) {
      this.items = items;
    }

    public void setCount(Long count) {
      this.count = count;
    }
  }
}
